Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | /* eslint-disable @typescript-eslint/no-unused-vars */
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';
import { Play, Info, Plus, Volume2, VolumeX } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import type { DiscoveryMediaItem } from '@/types';
import { ROUTES } from '@/constants/app';
type HeroSliderProps = {
items: DiscoveryMediaItem[];
className?: string;
intervalMs?: number;
onActiveVisualChange?: (bg: string) => void;
};
function resolveItemHref(item: DiscoveryMediaItem): string {
if (item.content_id) {
return ROUTES.CONTENT.WATCH(item.content_id);
}
const media = (item.media_type || '').toLowerCase();
if (media === 'tv') return `${ROUTES.CONTENT.BROWSE}?tab=shows&search=${encodeURIComponent(item.title)}`;
if (media === 'movie') return `${ROUTES.CONTENT.BROWSE}?tab=movies&search=${encodeURIComponent(item.title)}`;
return `${ROUTES.CONTENT.SEARCH}?q=${encodeURIComponent(item.title)}`;
}
function toBackgroundImage(src: string | undefined, fallback: string): string {
if (!src || !src.trim()) return fallback;
const trimmed = src.trim();
if (trimmed.startsWith('http') || trimmed.startsWith('/')) {
return `url(${trimmed})`;
}
return trimmed;
}
const FALLBACKS = [
'linear-gradient(135deg, #0f172a 0%, #000000 100%)',
];
export default function HeroSlider({ items, className, intervalMs = 8000, onActiveVisualChange }: HeroSliderProps) {
const { t } = useTranslation();
const [index, setIndex] = useState(0);
const [isMuted, setIsMuted] = useState(true);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const safeItems = useMemo(
() =>
(items || [])
.filter((it) => {
// Only require a usable visual
const hasVisual = !!(it.backdrop || it.image || (it as any).backdrop_url || (it as any).image_url);
return hasVisual;
})
.slice(0, 8),
[items],
);
const active = safeItems[index] || null;
// Auto-rotate
useEffect(() => {
if (safeItems.length <= 1) return;
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setIndex((i) => (i + 1) % safeItems.length);
}, intervalMs);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [safeItems.length, intervalMs]);
const backgroundImage = toBackgroundImage(
active?.backdrop || active?.image || (active as any)?.backdrop_url,
FALLBACKS[0],
);
// Notify parent
useEffect(() => {
onActiveVisualChange?.(backgroundImage);
}, [backgroundImage, onActiveVisualChange]);
if (!active) return null;
return (
<div className={cn("relative h-[75vh] w-full overflow-hidden", className)}>
{/* Background Layer */}
<div
key={active.id} // Force re-render for animation
className="absolute inset-0 bg-cover bg-center transition-all duration-1000 ease-in-out animate-in fade-in zoom-in-105"
style={{ backgroundImage }}
/>
{/* Gradient Overlays for Readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-[#0a0a0a]" />
<div className="absolute inset-0 bg-gradient-to-r from-[#0a0a0a] via-[#0a0a0a]/40 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-[#0a0a0a] via-transparent to-transparent" />
{/* Content Container */}
<div className="absolute inset-0 flex flex-col justify-end pb-20 sm:pb-24 px-6 sm:px-10 md:px-16">
<div className="max-w-3xl space-y-6 animate-in slide-in-from-bottom-10 duration-700 fade-in">
{/* Badges / Metadata */}
<div className="flex items-center gap-3 text-sm font-medium text-gray-300">
<span className="px-2 py-0.5 rounded text-xs font-bold bg-red-600 text-white uppercase tracking-wider">
{t('user.badges.featured')}
</span>
{active.year && (
<>
<span className="w-1 h-1 rounded-full bg-gray-400" />
<span>{active.year}</span>
</>
)}
{active.media_type && (
<>
<span className="w-1 h-1 rounded-full bg-gray-400" />
<span className="uppercase">{active.media_type}</span>
</>
)}
</div>
{/* Title */}
<h1 className="text-4xl sm:text-5xl md:text-7xl font-extrabold text-white tracking-tight drop-shadow-2xl leading-[1.1]">
{active.title}
</h1>
{/* Description */}
<p className="text-base sm:text-lg md:text-xl text-gray-200 line-clamp-3 max-w-2xl leading-relaxed drop-shadow-md text-shadow-sm">
{active.description || t('user.dashboard.welcomeDescription')}
</p>
{/* Actions */}
<div className="flex flex-wrap items-center gap-4 pt-2">
<Link href={resolveItemHref(active)}>
<Button
size="lg"
className="h-12 px-8 bg-white text-black hover:bg-gray-200 font-bold text-base rounded-md flex items-center gap-2 transition-transform hover:scale-105"
>
<Play className="h-5 w-5 fill-black" />
{t('user.dashboard.watchNow')}
</Button>
</Link>
<Button
size="lg"
variant="secondary"
className="h-12 px-8 bg-white/20 text-white hover:bg-white/30 backdrop-blur-md font-semibold text-base rounded-md flex items-center gap-2 border border-white/10 transition-transform hover:scale-105"
>
<Info className="h-5 w-5" />
{t('user.common.moreInfo')}
</Button>
</div>
</div>
</div>
{/* Right Side Controls (Mute, etc) */}
<div className="absolute bottom-24 right-6 sm:right-10 z-20 hidden md:flex items-center gap-4">
<button
onClick={() => setIsMuted(!isMuted)}
className="p-3 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm text-white border border-white/10 transition-colors"
>
{isMuted ? <VolumeX className="h-5 w-5" /> : <Volume2 className="h-5 w-5" />}
</button>
<div className="w-px h-8 bg-white/20" />
<div className="text-xs font-bold text-white/50 uppercase tracking-widest">
{active.media_type === 'movie' ? 'FILM' : 'SERIES'}
</div>
</div>
</div>
);
} |